Yes. Before P2SH, the most popular proposal for solving the problem that P2SH solves was OP_EVAL, which did just that: it repurposed OP_NOP1 as OP_EVAL.
A P2SH script looks like this:
[script input...] [serialized script] | OP_HASH160 [script hash] OP_EQUAL
The equivalent OP_EVAL script would look like this to old nodes:
[script input] [serialized script] | OP_DUP OP_HASH160 [script hash] OP_EQUALVERIFY OP_NOP1
Old nodes would only check that the serialized script matches the given script hash when hashed. The other stuff would be ignored.
New nodes would see:
[script input] [serialized script] | OP_DUP OP_HASH160 [script hash] OP_EQUALVERIFY OP_EVAL
New nodes would understand OP_EVAL, so they'd check that the serialized script matches the given script and execute the serialized script. An OP_EVAL transaction might be seen as invalid for new nodes and valid for old nodes, but not the other way around. That's what makes it a softfork. If the majority of mining power is using the new rules, then this is not a hardfork because the longest chain will always converge to a state that is valid for both old and new nodes.
Depending on the new opcode's behavior, it may be necessary for new nodes to verify scripts twice to ensure that certain scripts can't cause hardforks. Scripts are verified once using the old script rules and once using the new rules.
The OP_EVAL proposal was eventually replaced with P2SH because it was discovered that OP_EVAL accidentally allowed Turing-complete scripts, which the Bitcoin developers didn't want. P2SH is very simple (and a bit weird) in part so that similar accidents would be easy to avoid. But there wasn't any technical problem with this method of adding a new opcode. Moreover, there's nothing restricting the behavior of new opcodes. For example, OP_EVAL could have interpreted its input in a different, Turing-complete language, though there are various reasons why this might not have been a good idea.