In addition to workarounds, to make the request itself respond faster, did you consider that you maintain a column in the table that indicates whether it is in this set or not? This requires a lot of maintenance, but if the ExcludedCodes table does not change often, it might be better for this maintenance. For example, you can add a BIT column:
ALTER TABLE dbo.Table1 ADD IsExcluded BIT;
Make NOT NULL and the default value is 0. Then you can create a filtered index:
CREATE INDEX n ON dbo.Table1(name) WHERE IsExcluded = 0;
Now you just need to update the table once:
UPDATE t SET IsExcluded = 1 FROM dbo.Table1 AS t INNER JOIN dbo.ExcludedCodes AS x ON t.Code = x.Code;
And continuing, you will have to maintain this with triggers on both tables. In this case, your request will look like this:
SELECT @Count = COUNT(Name) FROM dbo.Table1 WHERE IsExcluded = 0;
EDIT
Regarding "NOT IN slower than LEFT JOIN", here is a simple test that I performed in just a few thousand lines:

EDIT 2
I'm not sure why this request will not do what you need and will be much more efficient than your 40K loop:
SELECT src.Name, COUNT(src.*) FROM dbo.Table1 AS src INNER JOIN
Or the equivalent of LEFT JOIN:
SELECT src.Name, COUNT(src.*) FROM dbo.Table1 AS src INNER JOIN
I would put money on any of these requests in less than 27 minutes. I would even suggest that executing both queries in sequence would be much faster than your single query, which takes 27 minutes.
Finally, you can consider the indexed view. I do not know your table structure and if you are not violating any restrictions, but it is worth exploring IMHO.