Сам Task, это класс с ~10 полями по 4 байта. Можно посчитать сколько займут в памяти 2000 штук. Очевидно проблема не в них. Как выше уже отметили, проблема не в самих тасках как таковых, а в том коде, который исполняется посредством тасков. И даже не в потоках. Попробуйте создать 2000 настоящих потоков (new System.Threading.Thread(...)) и выполнить там тривиальный код, навряд ли они смогут выжрать 10GB памяти.
Проблема в коде, который вы запускаете в этих тасках. Если вы не знакомы с автоматическими сброщиками мусора, то нужно про них почитать.
Любой код на C# - это плюс/минус метод. Создавая таск вы передаете ему ссылку на метод, который нужно выполнять, метод может быть именованным либо анонимным, неважно. Проблема в том, что по завершении этого метода, ресурсы, которые были использованы в этом методе не освобождаются. Неважно где исполняется метод: в таске, в потоке, просто так в основном потоке приложения. Вам нужно добиться того, чтобы при выходе из метода, которые вы передаете таску, все ресурсы использованные в этом методе были освобождены.
Если вы незнакомы со сборщиком мусора в CLR, то почитайте обязательно, да и вообще про сборку мусора.
В данном же ситуации, не видя кода, могу только посоветовать следующее. Далее для упрощения понимания будем считать, что у нас всего два участка кода: некий метод (это то что вы передаете в таск), и основной поток (вся остальная программа):
1) Если что-то создаете (вообще везде, а в методе особенно), любой класс, если у него есть метод .Close() или Dispose(), то обязательно вызывайте этот метод после того как класс вам больше не нужен.
2) Если есть возврат результата из метода, проверьте не возвращается ли сверх того, что вам нужно. Например, возвращается класс, с двумя полями, одно число, другое массив. Вам нужно из этого только число. Соответственно, поле с массивом нужно убрать из возвращаемого значения.
3) Упростите возвращаемый результат насколько это возможно. Например вам нужно подсчитать сумму элементов в N массивах. Вы запускаете N потоков и возвращаете N массивов, т.е. из каждого метода по массиву, а потом в основном потоке суммируете длины всех массивов. В этом случае как раз будет ощущение утечки памяти. Нужно возвращать сразу длину массива. И т.д.
4) Если есть добавление элементов из метода в коллекцию, которая объявлена в основном потоке. Проверить, очищается ли эта коллекция при выходе из метода. Или не добавляется и слишком много в эту коллекцию. Или Возможно в эту коллекцию добавляются слишком большие массивы и т.п.
5) Почти то же что и предыдущий пункт. Если есть какая-либо статическая коллекция или статические поля, переделайте на нестатические везде где возможно. А где невозможно, проследите чтобы в такую статическую коллекцию не добавлялись элементы из метода. Или если добавляются, то проверьте размер элементов, он должен быть минимальный.
6) Проследите, что не создаете больших массивов размером более 80кб. Если создаете, Измените на меньший размер если возможно. Например, если стоит задача подсчитать количество символов в файле, то не нужно читать его в память. Достаточно в цикле считывать по 8кб и суммировать результат.
7) Последнее. Перед выходом из метода вставьте:
System.Runtime.GCSettings.LargeObjectHeapCompactionMode = System.Runtime.GCLargeObjectHeapCompactionMode.CompactOnce;
System.GC.Collect();