I've never dealt with Python, so I am just trying to understand what's going on, visually/logically. Overall it looks fine, apart from the fact that LS suggest to only use the in-the-money paths for the regression, can't see that in your code, or am I missing it?
A few side notes, your variable naming is a little non-standard, I would change your T -> NT, t -> T. Also what you call discount rate is really usually called a discount factor. Finally you are trying to price an American(Bermudan) call with no dividends, which should have the same price as the equivalent European call. 

The LS algorithm, if done properly, **should** underprice your Americans anyway. That's because the continuation value approximation via the basis functions, is just that, an approximation. Which means that the algo (which bases its decision on the cont. value) will not always take the correct (optimal) decision to exercise, which then means the option value will be slightly less than if you had always exercised optimally.

Now, as Bob Jansen says, it may well be that you simply have too much noise. I mean, who uses 10.000 MC paths nowadays, that's so 90's:) Maybe because you're using Python and it's too slow for MC? I guess so. So, I tried your problem with [this tool][1] and with 8.000 (Sobol) paths (for both the main simulation and the regression) and 6 basis functions and I get C=21.32 (in half a sec). Then tried with 131.000 paths and I get 21.02 (in 12 secs). The correct price is 21.046. So yes, it may well be that you just have too much noise with so few paths and you cannot draw any conclusion as to whether you have some other problem or not.


  [1]: https://www.acenumerics.com/option-pricer.html