Based on the comments, I came to the following solution.
First, I render a LUID of 8 characters as described by nathanchere in this SO thread.
public class LUID
{
    private static readonly RNGCryptoServiceProvider RandomGenerator 
               = new RNGCryptoServiceProvider();
    private static readonly char[] ValidCharacters = 
               "ABCDEFGHJKLMNPQRSTUVWXYZ23456789".ToCharArray();
    public const int DefaultLength = 6;
    private static int counter = 0;
    public static string Generate(int length = DefaultLength)
    {
        var randomData = new byte[length];
        RandomGenerator.GetNonZeroBytes(randomData);
        var result = new StringBuilder(DefaultLength);
        foreach (var value in randomData)
        {
            counter = (counter + value) % (ValidCharacters.Length - 1);
            result.Append(ValidCharacters[counter]);
        }
        return result.ToString();
    }
}
Then I append a check digit calculated with the iso7064 MOD 1271,36 standard as described here.
I also added a small check that if the calculated check digit contains O, 0, I or 1, I regenerate the code again until it doesn't contain these characters anymore.
The result is a code like 6VXA35YDCE that is fairly unique (if my math is correct there should be like 1.099.511.627.776 possible combinations). It also does not contain I, 1, O and 0; to avoid confusion.
Also, I make sure that any generated LUID does not exist yet in the database before I create a new user with it, just to be sure that it keeps working in case collisions occur.
I think this meets the requirements I was looking for...