Your code works as expected and is correct. IMO the problem occurs because of the WMI property ParentProcessId. MSDN says:
ParentProcessId
Data type: uint32
Access type: Read-only
Unique identifier of the process that creates a process.
Process identifier numbers are reused, so they only identify
a process for the lifetime of that process. It is possible that
the process identified by ParentProcessId is terminated, so
ParentProcessId may not refer to a running process. It is also
possible that ParentProcessId incorrectly refers to a process
that reuses a process identifier. You can use the CreationDate
property to determine whether the specified parent was created
after the process represented by this Win32_Process instance
was created.
I assume, that your HashSet holds at some point ProcessId's that where replaced by the system with new processes and the new processes and not child processes anymore but are still in the collection and are terminated when fetched from the list.
You could extensively log every call of the process.Kill()
(name, process id, timestamp, and so on) and then try to track the problem using the log.